package nachos.network;

import java.util.*;

import nachos.machine.*;
import nachos.threads.*;

/**
 * A collection of message queues, one for each local port. A
 * <tt>PostOffice</tt> interacts directly with the network hardware. Because
 * of the network hardware, we are guaranteed that messages will never be
 * corrupted, but they might get lost.
 *
 * <p>
 * The post office uses a "postal worker" thread to wait for messages to arrive
 * from the network and to place them in the appropriate queues. This cannot
 * be done in the receive interrupt handler because each queue (implemented
 * with a <tt>SynchList</tt>) is protected by a lock.
 */
public class PostOffice {
    /**
     * Allocate a new post office, using an array of <tt>SynchList</tt>s.
     * Register the interrupt handlers with the network hardware and start the
     * "postal worker" thread.
     */
    public PostOffice() {
	messageReceived = new Semaphore(0);
	messageSent = new Semaphore(0);
	sendLock = new Lock();

    waitingAccept = new LinkedList[MailMessage.portLimit];

        for (int i = 0; i < MailMessage.portLimit; i++) {
            waitingAccept[i] = new LinkedList<Connection>();
        }

	queues = new SynchList[MailMessage.portLimit];
	for (int i=0; i<queues.length; i++)
	    queues[i] = new SynchList();

	Runnable receiveHandler = new Runnable() {
	    public void run() { receiveInterrupt(); }
	};
	Runnable sendHandler = new Runnable() {
	    public void run() { sendInterrupt(); }
	};
	Machine.networkLink().setInterruptHandlers(receiveHandler,
						   sendHandler);

	KThread t = new KThread(new Runnable() {
		public void run() { postalDelivery(); }
	    });

	t.fork();

    sendThread = new SendThread();
    sendThread.fork();
    
    for (int i = 0; i < MailMessage.portLimit; i++) {
    	portNums.add(i);
    }

    }

    public void addWaiting(Connection c) {
        waitingAccept[c.dstPort].add(c);
    }

    /**
     * Retrieve a message on the specified port, waiting if necessary.
     *
     * @param	port	the port on which to wait for a message.
     *
     * @return	the message received.
     */
    public MailMessage receive(int port) {
	Lib.assertTrue(port >= 0 && port < queues.length);

	Lib.debug(dbgNet, "waiting for mail on port " + port);

	MailMessage mail = (MailMessage) queues[port].removeFirst();

	if (Lib.test(dbgNet))
	    System.out.println("got mail on port " + port + ": " + mail);

	return mail;
    }

    /**
     * Wait for incoming messages, and then put them in the correct mailbox.
     */
    private void postalDelivery() {
	while (true) {
       //System.out.println( "Postal Delivery: waiting for messageReceived" );
	    messageReceived.P();
       //System.out.println( "Postal Delivery!!" );

	    Packet p = Machine.networkLink().receive();

	    MailMessage mail;

	    try {
		mail = new MailMessage(p);
            //Lib.debug('n', "Message Received: " + mail );
	    }
	    catch (MalformedPacketException e) {
            Lib.debug('n', "Bad Message Format!!");
		continue;
	    }

	    /*if (Lib.test(dbgNet))
            System.out.println("delivering mail to port " + mail.dstPort
                       + ": " + mail);*/

	    //long id = Connection.makeID(mail.dstLink, mail.dstPort, mail.srcLink, mail.srcPort);
        long id = Connection.makeID(mail.srcLink, mail.srcPort, mail.dstLink, mail.dstPort);
        
            if (!connections.containsKey(id)) {
                Connection c = null;
                c = new Connection(mail.dstLink, mail.dstPort, mail.srcLink, mail.srcPort, true);
                connections.put(id, c);
                c.addMessage(mail);
            }
            else
            {
                connections.get(id).addMessage(mail);
            }
	}
    }

    /**
     * Called when a packet has arrived and can be dequeued from the network
     * link.
     */
    private void receiveInterrupt() {
        //Lib.debug('n', "Receive Interrupt");
        messageReceived.V();
    }

    /**
     * Send a message to a mailbox on a remote machine.
     */
    public void send(MailMessage mail) {
	sendLock.acquire();

	Machine.networkLink().send(mail.getPacket());

	messageSent.P();

	sendLock.release();
    }

    /**
     * Called when a packet has been sent and another can be queued to the
     * network link. Note that this is called even if the previous packet was
     * dropped.
     */
    private void sendInterrupt() {
	messageSent.V();
    }

    public Connection connect(int host, int port) {
        //TODO: assign port
        int out = getLocalPort();
        
        Lib.debug('n', "Connect: src " + Machine.networkLink().getLinkAddress() + ":" + out + " dst " + host + ":" + port);
        Connection c = new Connection(host, port, Machine.networkLink().getLinkAddress(), out, false);
        if (out != -1){
	        Lib.debug('n', "connect: makeID(): " + c.makeID());
	        connections.put(c.makeID(), c);
	        c.transition(Connection.CONNECT);
        }

        return c;
    }

    public Connection accept(int port) {
        if (waitingAccept[port].isEmpty()) {
            return null;
        } else {
            Connection s = waitingAccept[port].removeFirst();
            s.transition(Connection.ACCEPT);
            return s;
        }
    }

    private int getLocalPort() {
		// TODO: Figure out a better scheme to assign local ports
      //return localPortIterator++ % MailMessage.portLimit;
    	int temp = -1;
    	try{
    		temp = portNums.removeFirst();
    	} catch (Exception e)
    	{
    		temp = -1;
    	}
    	return temp;
   }

    protected SynchList[] queues;
    private Semaphore messageReceived;	// V'd when a message can be dequeued
    private Semaphore messageSent;	// V'd when a message can be queued
    private Lock sendLock;

    protected Hashtable<Long, Connection> connections = new Hashtable<Long, Connection>();
    protected LinkedList<Connection>[] waitingAccept;
    private static int localPortIterator = 0;
    
    protected LinkedList<Integer> portNums = new LinkedList<Integer>();

    // sending thread
    public SendThread sendThread = null;

    private static final char dbgNet = 'n';
}
